百度APP iOS端包体积50M优化实践(四)代码优化
The following article is from 百度App技术 Author RichardYang
一、前言
GEEK TALK
百度APP iOS端包体积优化实践系列文章回顾:
二、Link Map文件详解
GEEK TALK
2.1 简介
2.2 生成linkMap文件
2.3 LinkMap文件结构解析
丨2.3.1 基础信息
# Path: /Users/richard/Desktop/demo/DerivedData/demo/Build/Products/Debug-iphoneos/demo.app/demo
# Arch: arm64
丨2.3.2 Object文件列表
丨2.3.2 Section段表
丨2.3.4 Symbols
第一列是方法起始地址,通过这个地址我们可以查上面的段表;
第二列是大小,通过这个可以算出方法占用的大小;
第三列是归属的类(.o),值是具体编号,通过反查目标文件列表可以知道对应的类;
第四列是方法名称。
三、代码优化
GEEK TALK
3.1 无用类瘦身
丨3.1.1 静态检测获取无用类
方案介绍
第一、获取所有类地址,命令:otool -v -s __DATA __objc_classlist。
Contents of (__DATA,__objc_classlist) section
0000000100008238 00009980 00000001 000099d0 00000001
0000000100008248 00009a48 00000001 00009a98 00000001
0000000100008258 00009ac0 00000001 00009b38 00000001
第二、获取引用类的地址,命令:otool -v -s __DATA __objc_classrefs。
Contents of (__DATA,__objc_classrefs) section
000000010000990000000000 00000000 00000000 00000000
000000010000991000000000 00000000 000099d0 00000001
000000010000992000000000 00000000 00000000 00000000
第三、取差集,所有类的地址减去引用类的地址,拿到的就是未使用类的地址信息。
第四、符号化,遍历Linkmap和Mach-O文件可获取地址信息对应的具体类名,建立类和地址的映射关系,通过地址反解析出类名。
优缺点
对于存在引用关系但根本不会被调用的类,是无法被判断为无用类的。随着版本迭代,新老员工工作交接,很多功能的入口已经不存在了,相关的类也根本不会被调用,但是引用关系仍然保留。通过静态检测的方式,无法检测出这种情况。
静态检测无法适用于通过反射调用类及方法的场景。因为静态检测无法感知运行时的环境,无法预测哪些类或方法会被反射调用。因此,在这种情况下,静态检测将无法准确地检测出无用类或无用的方法。
丨3.1.2 动态检测获取无用类
方案介绍
// These are not emitted by the compiler and are never used in class_ro_t.
// Their presence should be considered in future ABI versions.
// class_t->data is class_rw_t, not class_ro_t
#define RW_REALIZED (1<<31)
// class is unresolved future class
#define RW_FUTURE (1<<30)
// class is initialized
#define RW_INITIALIZED (1<<29)
// class is initializing
#define RW_INITIALIZING (1<<28)
// class_rw_t->ro is heap copy of class_ro_t
#define RW_COPIED_RO (1<<27)
// class allocated but not yet registered
#define RW_CONSTRUCTING (1<<26)
// class allocated and registered
#define RW_CONSTRUCTED (1<<25)
// available for use; was RW_FINALIZE_ON_MAIN_THREAD
// #define RW_24 (1<<24)
// class +load has been called
#define RW_LOADED (1<<23)
#define W_INITIALIZED (1<<29)bool isinitialized() { return getMeta() -›data()-›flags & W_INITIALIZED;}
对于业务线代码没有侵入性; 没有性能损耗; 可以针对线上实际运行环境做检测;
丨3.1.3 手百采用的技术方案
3.2 无用模块瘦身
def find_class(base_link_map_file):
link_map_file = open(base_link_map_file, 'rb')
reach_files = 0
reach_sections = 0
reach_symbols = 0
files_map = {}
while 1:
line = link_map_file.readline()
line = line.decode('utf-8', errors='ignore')
if not line:
break
if line.startswith("#"):
if line.startswith("# Object files:"):
reach_files = 1
if line.startswith("# Sections"):
reach_sections = 1
if line.startswith("# Symbols"):
reach_symbols = 1
else:
if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:
index = line.find("]")
if index != -1:
tmpfile = line[index + 2:-1]
file = tmpfile.split("/")[-1]
frameworkIndex = file.find("(")
if frameworkIndex!= -1:
frameworkName = file[0: frameworkIndex]
className = file[frameworkIndex + 1:len(file)-1]
if files_map:
if frameworkName in files_map:
files_map[frameworkName] = files_map[frameworkName] + " , " + className
else:
files_map[frameworkName] = className
else:
files_map[frameworkName] = className
link_map_file.close()
return files_map
3.3 无用方法瘦身
3.4 精简重复代码
brew install pmd
//生成的 XML 文件内容如下,根据 file 标签信息就能定位到重复代码位置。
pmd cpd --files 扫描文件目录 --minimum-tokens 70 --language objectivec --encoding UTF-8 --format xml > repeat.xml
<pmd-cpd>
<duplication lines="16" tokens="162">
<file begintoken="16933" column="33" endcolumn="4" endline="28" endtoken="17094" line="13" path="path1">
<file begintoken="23979" column="47" endcolumn="4" endline="26" endtoken="24140" line="11" path="path2" />
<codefragment>
***************************
</codefragment>
</duplication>
</pmd-cpd>
3.5 工具方法瘦身
实现系统类的Category,如NSDate、UIImage、NSArray、NSDictionary的分类方法实现; 独立封装;
遍历LinkMap文件,挖掘出重复的Category,参考以下的脚本代码来实现此功能:
def get_files_map(base_link_map_file):
link_map_file = open(base_link_map_file, 'rb')
reach_files = 0
reach_sections = 0
reach_symbols = 0
files_map = {}
while 1:
line = link_map_file.readline()
line = line.decode('utf-8', errors='ignore')
if not line:
break
if line.startswith("#"):
if line.startswith("# Object files:"):
reach_files = 1
if line.startswith("# Sections"):
reach_sections = 1
if line.startswith("# Symbols"):
reach_symbols = 1
else:
if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:
# files
index = line.find("]")
if index != -1:
symbol = {"file": line[index + 2:-1]}
key = int(line[1: index])
files_map[key] = symbol
pass
link_map_file.close()
return files_map
对于非Category的工具方法,进行排查和合并,最终下沉到统一工具库里面。
3.6 AB实验固话
#define kFaceverifyResourceOptimizeABTestKey @"face_verify_resource_optimize_enable"
static let verifyResourceOptimizeABTestKey: String = "face_verify_resource_optimize_enable"
四、总结
GEEK TALK
END
参考资料:
推荐阅读: